/**
 * Copyright (c) iwindplus Technologies Co., Ltd.2024-2030, All rights reserved.
 */

package com.iwindplus.boot.web.flux.handler;

import com.iwindplus.boot.domain.constant.CommonConstant.NumberConstant;
import com.iwindplus.boot.domain.constant.CommonConstant.TraceConstant;
import com.iwindplus.boot.domain.exception.CommonException;
import com.iwindplus.boot.domain.vo.ResultVO;
import com.iwindplus.boot.util.CryptoUtil;
import com.iwindplus.boot.util.HttpsUtil;
import com.iwindplus.boot.util.JacksonUtil;
import com.iwindplus.boot.web.domain.property.GlobalErrorProperty;
import com.iwindplus.boot.web.domain.property.ResponseBodyProperty;
import io.swagger.v3.oas.annotations.Hidden;
import jakarta.annotation.Resource;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.MDC;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.http.HttpStatus;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ResponseStatusException;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebExceptionHandler;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

/**
 * reactor 端全局异常处理.
 *
 * @author zengdegui
 * @since 2024/06/19 23:08
 */
@Slf4j
@Hidden
@Order(Ordered.HIGHEST_PRECEDENCE)
@Component
@EnableConfigurationProperties(GlobalErrorProperty.class)
@ConditionalOnProperty(prefix = "global.error", name = "enabled", havingValue = "true", matchIfMissing = true)
public class GlobalErrorHandler implements WebExceptionHandler {

    @Resource
    private GlobalErrorProperty globalErrorProperty;

    @Resource
    private ResponseBodyProperty responseBodyProperty;

    @Override
    public Mono<Void> handle(ServerWebExchange exchange, Throwable throwable) {
        final URI uri = exchange.getRequest().getURI();
        final String realIp = Optional.ofNullable(MDC.get(TraceConstant.REAL_IP)).orElse(HttpsUtil.getRealIp(exchange));
        log.info("GlobalErrorHandler全局异常捕获, uri={}, realIp={}", uri, realIp);

        ResultVO<?> result;
        if (throwable instanceof CommonException exception) {
            log.warn("GlobalErrorHandler全局异常捕获，message={}", throwable.getMessage(), throwable);

            result = ResultVO.error(exception);
        } else if (throwable instanceof ResponseStatusException exception) {
            log.warn("GlobalErrorHandler全局异常捕获，message={}", throwable.getMessage(), throwable);

            final HttpStatus httpStatus = HttpStatus.valueOf(exception.getStatusCode().value());
            result = ResultVO.error(httpStatus);
        } else {
            log.error("GlobalErrorHandler全局异常捕获，message={}", throwable.getMessage(), throwable);

            result = ResultVO.error(HttpStatus.INTERNAL_SERVER_ERROR);
        }
        return this.responseData(exchange, globalErrorProperty, responseBodyProperty, null, result);
    }

    private Mono<Void> responseData(ServerWebExchange exchange, GlobalErrorProperty globalErrorProperty,
        ResponseBodyProperty responseBodyProperty, HttpStatusCode httpStatusCode, ResultVO<?> result) {
        httpStatusCode = this.getHttpStatusCode(globalErrorProperty, httpStatusCode, result);
        final Object data = this.getResult(responseBodyProperty, result);
        return this.getMonoResponse(exchange, httpStatusCode, data);
    }

    private HttpStatusCode getHttpStatusCode(GlobalErrorProperty globalErrorProperty, HttpStatusCode httpStatusCode, ResultVO<?> result) {
        // 启用异常状态码返回200
        if (Boolean.TRUE.equals(globalErrorProperty.getEnabled()) && Boolean.TRUE.equals(globalErrorProperty.getEnabledStatusOk())) {
            return HttpStatus.OK;
        }
        return Optional.ofNullable(httpStatusCode).orElse(result.getBizStatus() > HttpStatus.NETWORK_AUTHENTICATION_REQUIRED.value()
            ? HttpStatus.INTERNAL_SERVER_ERROR : HttpStatusCode.valueOf(result.getBizStatus()));
    }

    private Object getResult(ResponseBodyProperty responseBodyProperty, ResultVO<?> result) {
        // 判断是否启用响应解密
        if (Boolean.TRUE.equals(responseBodyProperty.getEnabled()) && Boolean.TRUE.equals(responseBodyProperty.getCrypto().getEnabled())) {
            return CryptoUtil.encrypt(JacksonUtil.toJsonStr(result), responseBodyProperty.getCrypto());
        }
        return result;
    }

    private Mono<Void> getMonoResponse(ServerWebExchange exchange, HttpStatusCode httpStatusCode, Object data) {
        ServerHttpResponse response = exchange.getResponse();
        response.setStatusCode(httpStatusCode);
        response.getHeaders().setContentType(MediaType.APPLICATION_JSON);

        // 将数据分成多个 DataBuffer 块
        Flux<DataBuffer> dataBufferFlux = this.splitDataIntoChunks(response, data);

        // 使用 Flux 发送数据块
        return response.writeWith(dataBufferFlux).doOnTerminate(() -> dataBufferFlux.subscribe(DataBufferUtils::release));
    }

    private Flux<DataBuffer> splitDataIntoChunks(ServerHttpResponse response, Object data) {
        // 实现将数据分成多个 DataBuffer 块的逻辑
        // 这里只是一个示例，具体实现取决于你的需求
        DataBufferFactory bufferFactory = response.bufferFactory();
        byte[] bytes = JacksonUtil.toJsonPrettyBytes(data);

        // 分块大小为 1024 字节
        int chunkSize = NumberConstant.NUMBER_ONE_THOUSAND_TWENTY_FOUR;
        int offset = 0;

        List<DataBuffer> chunks = new ArrayList<>(10);

        while (offset < bytes.length) {
            int remaining = bytes.length - offset;
            int size = Math.min(chunkSize, remaining);
            byte[] chunk = new byte[size];
            System.arraycopy(bytes, offset, chunk, 0, size);
            chunks.add(bufferFactory.wrap(chunk));
            offset += size;
        }

        return Flux.fromIterable(chunks);
    }
}
